.. _глава_73_start:

foreach со структурами и классами
=================================

.. index::
    foreach

Как вы помните из главы "Цикл foreach" (:ref:`Цикл foreach<глава_29_start>`), и то, как работает foreach
работает, так и типы и количество переменных цикла, которые он поддерживает, зависят от
типа коллекции: Для срезов foreach предоставляет доступ к элементам со счетчиком или без него.
Для ассоциативных массивов - к значениям с ключами или без них; для диапазонов чисел
для диапазонов чисел - к отдельным значениям. Для библиотечных типов foreach ведет себя учитывая специфику данного типа,
например, для File он выводит строки файла.
Можно определить поведение foreach и для пользовательских типов.
Существует два способа обеспечения такой поддержки:
* Определение функций-членов диапазона, что позволяет использовать пользовательский тип
с другими алгоритмами работы с диапазонами
* Определение одной или нескольких функций-членов opApply.
Из этих двух методов приоритет имеет opApply: Если она определена, компилятор использует
opApply, в противном случае он рассматривает функции-члены диапазона. Однако в большинстве
случаях функции-члены диапазона достаточны, а так же проще и полезнее.
foreach не обязательно должен поддерживаться для каждого типа. Итерация над объектом имеет смысл
имеет смысл только в том случае, если этот объект определяет концепцию коллекции.
Например, может быть неясно, какие элементы должен предоставлять foreach при
итерации над классом, который представляет студента, поэтому класс лучше не поддерживать
foreach вообще. С другой стороны, при проектировании может потребоваться, чтобы Student был
коллекция оценок, а foreach может предоставлять отдельные оценки студента.
От дизайна программы зависит, какие типы должны обеспечивать такую поддержку
и каким образом.


Поддержка foreach функциями-членами диапазона
---------------------------------------------

Мы знаем, что foreach очень похож на for, за исключением того, что он более полезен и
безопаснее, чем for. Рассмотрим следующий цикл:

.. code-block:: d

    foreach (element; myObject)
    {
    // ... expressions ...
    }

За кулисами компилятор переписывает этот цикл foreach в цикл for, примерно
эквивалентный следующему:

.. code-block:: d

    for ( ; /* while not done */; /* skip the front element */)
    {
    auto element = /* the front element */;
    // ... expressions ...
    }

Определяемые пользователем типы, которые должны поддерживать foreach, могут предоставлять три функции-члена
функции, которые соответствуют трем разделам предыдущего кода: определение
завершение цикла, пропуск первого элемента и предоставление доступа к
первому элементу.
Эти три функции-члена должны быть названы empty, popFront и front,
соответственно. Код, генерируемый компилятором, вызывает эти функции:

.. code-block:: d

    for ( ; !myObject.empty(); myObject.popFront())
    {
        auto element = myObject.front();
        // ... expressions ...
    }

Эти три функции должны работать в соответствии со следующими правилами:
* .empty() должна возвращать true, если цикл завершен, false - в противном случае
* .popFront() должна переходить к следующему элементу (другими словами, пропускать передний
элемент)
* .front() должна возвращать передний элемент.
Любой тип, определяющий эти функции-члены, может быть использован с foreach.

**Пример**

Давайте определим структуру, которая выдает числа в определенном диапазоне. Для того чтобы
соответствовать диапазонам чисел и индексам срезов D, пусть последнее число
находится за пределами допустимых чисел. Согласно этим требованиям, следующая структура
будет работать точно так же, как диапазоны чисел D

.. code-block:: d

    struct NumberRange
    {
        int begin;
        int end;
        invariant ()
        {
            // There is a bug if begin is greater than end
            assert(begin <= end);
        }

        bool empty() const
        {
            // The range is consumed when begin equals end
            return begin == end;
        }

        void popFront()
        {
            // Skipping the first element is achieved by
            // incrementing the beginning of the range
            ++begin;
        }

        int front() const
        {
            // The front element is the one at the beginning
            return begin;
        }
    }


Примечание: безопасность этой реализации зависит только от единственного инвариантного блока.
Можно добавить дополнительные проверки в функцииfront иpopFront, чтобы гарантировать, что эти
функции никогда не вызывались, если диапазон пуст.
Объекты этой структуры можно использовать в foreach:

.. code-block:: d

    foreach (element; NumberRange(3, 7))
    {
        write(element, ' ');
    }

foreach использует эти три функции за кулисами и выполняет итерацию до тех пор, пока empty()
возвращает true:

.. code-block:: console

    3 4 5 6

.. index::
    std.range
    std.range.retro

**std.range.retro для обратной итерации**

Модуль std.range содержит множество алгоритмов работы с диапазонами. retro - один из таких алгоритмов.
алгоритмов, который выполняет итерацию диапазона в обратном порядке. Для его работы требуется две дополнительные
функции-члена range:
* .popBack() должна переходить к элементу, который находится за один до конца (пропускает
последний элемент)
* .back() должна возвращать последний элемент.
Однако, хотя это и не имеет прямого отношения к обратной итерации, для того чтобы retro рассматривал
эти функции вообще, должна быть определена еще одна функция:
* .save() должна возвращать копию данного объекта.
Подробнее об этих функциях-членах мы узнаем позже в главе "Диапазоны (:ref:`Диапазоны<глава_81_start>`).
Эти три дополнительные функции-члена могут быть тривиально определены для
NumberRange

.. code-block:: d

    struct NumberRange
    {
        // ...
        void popBack()
        {
            // Skipping the last element is achieved by
            // decrementing the end of the range.
            --end;
        }

        int back() const
        {
            // As the 'end' value is outside of the range, the
            // last element is one less than that
            return end - 1;
        }

        NumberRange save() const
        {
            // Returning a copy of this struct object
            return this;
        }
    }

Объекты этого типа теперь можно использовать с retro:

.. code-block:: d

    import std.range;
    // ...
    foreach (element; NumberRange(3, 7).retro) {
        write(element, ' ');
    }

Вывод теперь идёт в обратном порядке:

.. code-block:: console

    6 5 4 3

.. index::
    opApply
    opApplyReverse

Поддержка foreach в функциях opApply и opApplyReverse
------------------------------------------------------

Все, что сказано об opApply в этом разделе, справедливо и для opApplyReverse
также. opApplyReverse предназначена для определения поведения объектов в
циклов foreach_reverse.
Приведенные выше функции-члены позволяют использовать объекты в качестве диапазонов. Этот метод
более подходящим, когда существует только один разумный способ итерации по диапазону. Для примера
например, легко предоставить доступ к отдельным студентам типа Students
типа Students.
С другой стороны, иногда имеет смысл выполнять итерации по одному и тому же
объектом разными способами. Мы знаем это на примере ассоциативных массивов, где можно
получить доступ либо только к значениям, либо как к ключам, так и к значениям:
foreach со структурами и классами:


.. code-block:: d

    string[string] dictionary; // from English to Turkish
    // ...
    foreach (inTurkish; dictionary)
    {
    // ... only values ...
    }
    foreach (inEnglish, inTurkish; dictionary)
    {
    // ... keys and values ...
    }

opApply позволяет использовать пользовательские типы с foreach различными и иногда
более сложными способами. Прежде чем узнать, как определить opApply, мы должны сначала
понять, как он автоматически вызывается foreach.
При выполнении программы происходит чередование выражений внутри блока foreach
и выражениями внутри функции opApply(). Сначала вызывается функция-член opApply()
а затем opApply делает явный вызов блока
блоку foreach. Так они чередуются до тех пор, пока цикл в конце концов не завершится.
Этот процесс основан на соглашении, которое я скоро объясню.
Сначала давайте еще раз посмотрим на структуру цикла foreach:

.. code-block:: d

    // The loop that is written by the programmer:
    foreach (/* loop variables */; myObject)
    {
    // ... expressions inside the foreach block ...
    }

Если существует функция-член opApply(), которая соответствует переменным цикла, то
блок foreach становится делегатом, который затем передается в opApply().
Соответственно, приведенный выше цикл преобразуется в следующий код за  кулисами.
Фигурные скобки, определяющие тело делегата, выделены:

.. code-block:: d

    / The code that the compiler generates behind the scenes:
    myObject.opApply(delegate int(/* loop variables */)
    {
    // ... expressions inside the foreach block ...
    return hasBeenTerminated;
    });

Другими словами, цикл foreach заменяется делегатом, который передается в
opApply(). Прежде чем показать пример, приведем требования и
требования и ожидания этого соглашения, которые должен соблюдать opApply():
#. Тело цикла foreach становится телом делегата. opApply должен
вызывать этот делегат на каждой итерации.
#. Переменные цикла становятся параметрами делегата. opApply() должен
определить эти параметры как ref. (Переменные могут быть определены и без ключевого слова ref.
но это не позволит выполнять итерации по элементам по ссылке (:ref:`Типы значения и ссылочные типы<глава_34_start>`).)
#. Возвращаемый тип делегата - int. Соответственно, компилятор вставляет
оператор return в конце делегата, который определяет, был ли цикл
цикл был завершен (прерыванием или оператором возврата): Если возвращаемое
значение равно нулю, итерация должна быть продолжена, в противном случае она должна завершиться.
#. Собственно итерация происходит внутри opApply().
#. opApply() должна возвращать то же значение, которое возвращает делегат.

Ниже приведено определение NumberRange, реализованное в соответствии с
этой конвенции:

.. code-block:: d

    struct NumberRange
    {
        int begin;
        int end;
        //    (2)       (1)
        int opApply(int delegate(ref int) operations) const
        {
            int result = 0;
            for (int number = begin; number != end; ++number)
            { // (4)
                result = operations(number); // (1)
                if (result)
                {
                    break; // (3)
                }
            }
            return result; // (5)
        }
    }

Это определение NumberRange можно использовать с foreach точно так же.
как и раньше:

.. code-block:: d

    foreach (element; NumberRange(3, 7))
    {
    write(element, ' ');
    }

Вывод такой же, как и при использовании функций-членов диапазона:


.. code-block:: console

    3 4 5 6

**Перегрузка opApply для выполнения итерации разными способами**

Можно выполнять итерации над одним и тем же объектом разными способами, определяя
перегрузки opApply(), которые принимают различные типы делегатов. Компилятор вызывает
ту перегрузку, которая соответствует определенному набору переменных цикла.
В качестве примера сделаем возможным итерацию над NumberRange по двум переменным цикла
переменных:

.. code-block:: d

    foreach (first, second; NumberRange(0, 15))
    {
        writef("%s,%s ", first, second);
    }


Обратите внимание, что это похоже на способ итерации ассоциативных массивов как по ключам
и значениям.
Для этого примера потребуем, чтобы при итерации объекта NumberRange по
двум переменным, он должен выдавать два последовательных значения и произвольно
увеличивает значения на 5. Таким образом, цикл выше должен выдавать следующий результат:

.. code-block:: d

    0,1 5,6 10,11

Это достигается дополнительным определением функции opApply(), которая принимает делегата
который принимает два параметра. opApply() должен вызывать этот делегат с двумя значениями:

.. code-block:: d

    int opApply(int delegate(ref int, ref int) dg) const
    {
        int result = 0;
        for (int i = begin; (i + 1) < end; i += 5)
        {
            int first = i;
            int second = i + 1;
            result = dg(first, second);
            if (result)
            {
                break;
            }
        }
        return result;
    }

При наличии двух переменных цикла вызывается эта перегрузка opApply().
Перегрузок opApply() может быть столько, сколько необходимо.
Можно, а иногда и нужно, давать компилятору подсказки о том, какую
какую перегрузку выбрать. Это делается путем указания типов переменных цикла
в явном виде.
Например, предположим, что существует тип School, который поддерживает итерацию
над учителями и учениками по отдельности:

.. code-block:: d

    class School
    {
        int opApply(int delegate(ref Student) dg) const
        {
            // ...
        }

        int opApply(int delegate(ref Teacher) dg) const
        {
            // ...
        }
    }

Чтобы указать желаемую перегрузку, необходимо указать переменную цикла:

.. code-block:: d

    foreach (Student student; school)
    {
    // ...
    }
    foreach (Teacher teacher; school)
    {
    // ...
    }

Счетчик цикла
--------------

Удобный счетчик циклов для фрагментов не является автоматическим для других типов. Счетчик циклов может быть реализован для пользовательских типов различными способами в зависимости от того.
обеспечивается ли поддержка foreach функциями-членами диапазона или
opApply перегрузки.

**Счетчик циклов с функциями диапазона**

Если поддержка foreach обеспечивается функциями-членами диапазона, то счетчик циклов
можно получить с помощью функции enumerate из модуля std.range:import std.range;

.. code-block:: d

    // ...
    foreach (i, element; NumberRange(42, 47).enumerate)
    {
        writefln("%s: %s", i, element);
    }

enumerate - это диапазон, который выдает последовательные числа, начинающиеся по умолчанию с
0. enumerate сопоставляет каждое число с элементами диапазона, к которому оно применяется
к нему. В результате числа, которые генерирует enumerate, и элементы
реального диапазона (в данном случае NumberRange) появляются в цикле в качестве переменных:

.. code-block:: console

    0: 42
    1: 43
    2: 44
    3: 45
    4: 46

**Счетчик циклов с opApply**

С другой стороны, если поддержка foreach обеспечивается opApply(), то счетчик цикла
должен быть определен как отдельный параметр делегата, подходящий по типу
size_t. Рассмотрим это на примере структуры, представляющей цветной многоугольник.
Как мы уже видели выше, функцию opApply(), предоставляющую доступ к точкам
этого многоугольника, может быть реализована без счетчика, как в следующем коде:

.. code-block:: d

    import std.stdio;

    enum Color
    {
        blue,
        green,
        red
    }

    struct Point
    {
        int x;
        int y;
    }

    struct Polygon
    {
        Color color;
        Point[] points;
        int opApply(int delegate(ref const(Point)) dg) const
        {
            int result = 0;
            foreach (point; points)
            {
                result = dg(point);
                if (result)
                {
                    break;
                }
            }
            return result;
        }
    }

    void main()
    {
        auto polygon = Polygon(Color.blue,
            [Point(0, 0), Point(1, 1)]);
        foreach (point; polygon)
        {
            writeln(point);
        }
    }

Обратите внимание, что сама функция opApply() реализована в цикле foreach. В результате
foreach внутри main() в итоге косвенно использует foreach над членом
точки.
Также обратите внимание, что тип параметра делегата - ref const(Point). Это
означает, что данное определение opApply() не позволяет изменять элементы Point
элементов многоугольника. Чтобы позволить пользовательскому коду изменять элементы, обе
сама функция opApply() и параметр делегата должны быть определены
без спецификатора const.
Выход:

.. code-block:: console

    const(Point)(0, 0)
    const(Point)(1, 1)


Естественно, попытка использовать это определение Polygon со счетчиком цикла
приведет к ошибке компиляции:

.. code-block:: d

    foreach (i, point; polygon) // ← ошибка компиляции
    {
        writefln("%s: %s", i, point);
    }

Ошибка компиляции:

.. code-block:: console

    Error: cannot uniquely infer foreach argument types

Чтобы это работало, необходимо определить другую перегрузку opApply(), поддерживающую счетчик
определена:

.. code-block:: d

    int opApply(int delegate(ref size_t,
            ref const(Point)) dg) const
    {
        int result = 0;
        foreach (i, point; points)
        {
            result = dg(i, point);
            if (result)
            {
                break;
            }
        }
        return result;
    }

На этот раз переменные foreach соответствуют новой перегрузке opApply(), и
программа выводит желаемый результат:

.. code-block:: console

    0: const(Point)(0, 0)
    1: const(Point)(1, 1)


Обратите внимание, что эта реализация opApply() использует преимущества автоматического
счетчика над членом points. (Хотя переменная делегата определена как ref
size_t, циклforeach внутри функцииidemain() не может модифицировать переменную счетчика по
points).
При необходимости счетчик цикла может быть определен и увеличен в явном виде
также. Например, поскольку следующая операцияApply() реализуется с помощью оператора while
она должна определять отдельную переменную для счетчика:

.. code-block:: d

    int opApply(int delegate(ref size_t,
            ref const(Point)) dg) const
    {
        int result = 0;
        bool isDone = false;
        size_t counter = 0;
        while (!isDone)
        {
            // ...
            result = dg(counter, nextElement);
            if (result)
            {
                break;
            }
            ++counter;
        }
        return result;
    }


Предупреждение: Коллекция не должна мутировать во время итерации
----------------------------------------------------------------

Независимо от того, обеспечивается ли поддержка итерации функциями-членами range
или функциями opApply(), сама коллекция не должна изменяться. Новый
не должны добавляться в контейнер, а существующие элементы не должны
удаляться. (Мутирование существующих элементов допускается).
Иное поведение является неопределенным.


Упражнения
----------

#. Разработайте структуру, работающую аналогично NumberRange, которая также поддерживает
указание размера шага. Размер шага может быть третьим членом:

.. code-block:: d

    foreach (element; NumberRange(0, 10, 2))
    {
        write(element, ' ');
    }

Ожидаемый результат приведенного выше кода - каждое второе число от 0 до 10:

.. code-block:: console

    0 2 4 6 8

#. Реализуйте класс School, который упоминался в тексте, таким образом, чтобы он
предоставляет доступ к ученикам или учителям в зависимости от переменной foreach.
Решения приведены на странице ???.

